package com.ruoyi.handler.websocket;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.TypeReference;
import com.ruoyi.common.config.EnvConfig;
import com.ruoyi.common.constant.WebSocketMsgContants;
import com.ruoyi.common.core.domain.entity.wsbean.PingMsg;
import com.ruoyi.common.core.domain.entity.wsbean.ReceivedTextMsg;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.manager.WebSocketChannelManager;
import com.ruoyi.web.service.TokenService;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.Attribute;
import io.netty.util.AttributeKey;
import lombok.extern.slf4j.Slf4j;

import java.util.Objects;
import java.util.Timer;
import java.util.TimerTask;

/**
 * 自定义WebSocket处理程序
 * SimpleChannelInboundHandler的泛型需要传递WebSocketFrame
 *
 * @author Nanase Takeshi
 * @date 2022/5/7 15:07
 */
@Slf4j
//@ChannelHandler.Sharable
public class MyWebSocketChannelInboundHandler extends SimpleChannelInboundHandler<WebSocketFrame> {

    private Timer keepAliveTimer;
    private TimerTask keepWebSocketAliveTimerTask;

    class KeepWebSocketAliveTimerTask extends TimerTask {
        private Channel channel;

        KeepWebSocketAliveTimerTask(Channel channel) {
            this.channel = channel;
        }

        @Override
        public void run() {
            log.info("KeepWebSocketAliveTimerTask run.");
            if (Objects.nonNull(channel)) {
                log.warn("持续指定时长[{}秒]后仍未收到ws客户端{}的心跳报文，服务器主动断开当前连接！", EnvConfig.wsHeartBeatDuration + 10, channel);
                if (channel.isActive()) {
                    //调用如下close方法后就会自动触发MyWebSocketChannelInboundHandler中的channelInactive方法，
                    // 进而执行WebSocketChannelManager.handleWebsocketChannelInactive()方法
//                    channel.close();
                    WebSocketMessageHandler.sendCloseChannelMsg(channel, String.format("持续指定时长[%d秒]后仍未收到ws客户端%s的心跳报文，服务器主动断开当前连接！", EnvConfig.wsHeartBeatDuration + 10, channel.remoteAddress().toString()));
                } else {
                    WebSocketChannelManager.handleWebsocketChannelInactive(channel);
                }
            }
        }
    }

    /**
     * 主动取消保活计时器。
     */
    public void cancelKeepAliveTimer(Channel channel) {
        log.info("cancelKeepAliveTimer execute.");
        if (Objects.nonNull(keepAliveTimer) && Objects.nonNull(keepWebSocketAliveTimerTask)) {
            log.info("task即将执行的剩余时长: {}, Thread id: {}, ws客户端[{}]", keepWebSocketAliveTimerTask.scheduledExecutionTime() - System.currentTimeMillis(),
                    Thread.currentThread().getId(), channel.remoteAddress().toString());
            keepWebSocketAliveTimerTask.cancel();
            keepAliveTimer.cancel();
            keepAliveTimer.purge();
            keepAliveTimer = null;
        }
    }

    /**
     * 事件回调
     *
     * @param ctx
     * @param evt
     * @throws Exception
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
            //协议握手成功完成
            log.info("MyWebSocketChannelInboundHandler.userEventTriggered --> : websocket协议握手成功完成");

            //从通道中获取用户token
            AttributeKey<String> tokenAttributeKey = AttributeKey.valueOf("token");
            AttributeKey<String> userAgentAttributeKey = AttributeKey.valueOf("userAgent");
            String token = ctx.channel().attr(tokenAttributeKey).get();
            String userAgent = ctx.channel().attr(userAgentAttributeKey).get();
            log.info("MyWebSocketChannelInboundHandler.userEventTriggered --> : token: {}, userAgent: {}", token, userAgent);
            //校验token逻辑,根据token从redis中尝试获取用户身份信息
            TokenService tokenService = SpringUtils.getBean(TokenService.class);
            LoginUser loginUser;
            if (isMiniProgramDevice(userAgent)) { //微信小程序客户端
                loginUser = tokenService.getMiniprogramLoginUser(token);
            } else {
                loginUser = tokenService.getLoginUser(token);
            }
//            log.info("userEventTriggered loginUser: {}", loginUser);

            if (loginUser == null) { //token校验失败
                log.warn("token无效或已过期,服务器断开当前websocket连接.");
                //如果token校验不通过，发送连接关闭的消息给客户端，设置自定义code和msg用来区分下服务器是因为token不对才导致关闭
                WebSocketMessageHandler.sendCloseChannelMsgAfterTokenExpired(ctx.channel(), "token无效或已过期,服务器断开当前websocket连接.");
            } else { //token校验已通过
                //执行到此处，说明token校验已通过
                log.info("MyWebSocketChannelInboundHandler.userEventTriggered --> : token校验成功, 当前websocket连接已建立！ channel: {}", ctx.channel());
                WebSocketChannelManager.addChannel(token, loginUser, ctx.channel());
            }
        }
        //通过判断IdleStateEvent的状态来实现自己的读空闲，写空闲，读写空闲处理逻辑
        if (evt instanceof IdleStateEvent && ((IdleStateEvent) evt).state() == IdleState.READER_IDLE) {
            //读空闲，关闭通道
            log.info("MyWebSocketChannelInboundHandler.userEventTriggered --> : 读空闲，关闭通道");
            ctx.close();
        }
    }

    /**
     * 判断当前登录的设备是否是微信小程序客户端。
     *
     * @param userAgent
     * @return
     */
    private boolean isMiniProgramDevice(String userAgent) {
        //微信小程序调试工具：
        //  User-Agent: wechatdevtools desktopapp appservice port/59291 token/8c2037b509c03c3e94be581fa5a1ba1f runtime/2
        //微信小程序开发版：
        // userAgent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.39(0x18002733) NetType/WIFI Language/zh_CN
        if (userAgent.contains("wechatdevtools") || userAgent.contains("Mobile")) {
            return true;
        }

        return false;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame webSocketFrame) throws Exception {
        log.info("接收到客户端发送的消息：{}", webSocketFrame);
        if (webSocketFrame instanceof TextWebSocketFrame) {
            TextWebSocketFrame textWebSocketFrame = (TextWebSocketFrame) webSocketFrame;
            String jsonMsg = textWebSocketFrame.text();
            log.info("channelRead0 msg: {}", jsonMsg);

            AttributeKey<LoginUser> attributeKey = AttributeKey.valueOf(MyWebSocketServerProtocolHandler.LOGIN_USER_KEY);
            LoginUser loginUser = ctx.channel().attr(attributeKey).get();
//            log.info("channelRead0 loginUser= {}", loginUser);

            parseTextMsg(ctx.channel(), jsonMsg);
        }
    }

    public void parseTextMsg(Channel channel, String jsonMsg) {
        JSONObject jsonObject = JSONObject.parseObject(jsonMsg);

        ReceivedTextMsg basicTextMsg;
        int code = (int) jsonObject.get("code");

        switch (code) {
            case WebSocketMsgContants.MSG_PING: //接收到websocket客户端(包含小程序与web前端)发送过来的心跳包
                log.info("接收到ws客户端[{}]的心跳包. Thread id: {}", channel.remoteAddress().toString(), Thread.currentThread().getId());
                if (Objects.nonNull(keepAliveTimer)) {
                    cancelKeepAliveTimer(channel);
                }
                keepAliveTimer = new Timer();
                keepWebSocketAliveTimerTask = new KeepWebSocketAliveTimerTask(channel);
                //如果指定时长后定时任务被执行，说明此指定时长内未再收到当前客户端的状态报文，此客户端应该已断开连接，服务器需主动关闭此连接。
                keepAliveTimer.schedule(keepWebSocketAliveTimerTask, EnvConfig.wsHeartBeatDuration * 1000 + 10000);

                basicTextMsg = JSON.parseObject(jsonMsg, new TypeReference<ReceivedTextMsg<PingMsg>>() {
                });
                log.info("basicTextMsg: {}", basicTextMsg);
                WebSocketMessageHandler.pong(channel);
                break;
            default:
                basicTextMsg = new ReceivedTextMsg<>();
        }

    }

    /**
     * 连接断开后的回调函数。
     * 常见触发场景：
     * 1，客户端停止运行；
     * 2，客户端主动调用websocket的close方法关闭连接；
     * 3，服务器接收数据后处理异常；
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        log.info("MyWebSocketChannelInboundHandler.channelInactive: 通道已关闭！channel: {}", channel);
        //获取用户关闭通道时设置的close code
        AttributeKey<Integer> attributeKey = AttributeKey.valueOf("closeCode");
        Attribute<Integer> closeCode = ctx.channel().attr(attributeKey);
        //对closeCode进行区分处理....

        //主动取消保活计时器
        cancelKeepAliveTimer(channel);

        WebSocketChannelManager.handleWebsocketChannelInactive(channel);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("MyWebSocketChannelInboundHandler.exceptionCaught --> channel: {}, cause: {}", ctx.channel(), cause.getMessage());
        ctx.close();
        //实测该异常捕获方法中调用ctx.close方法后貌似通常都会自动调用channelInactive方法，所以在当前方法中就没必要移除channel
//        WebSocketChannelManager.removeChannel(ctx.channel());
    }

}
